/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.persistence.jcr.manager.impl;

import java.io.FileNotFoundException;
import java.util.Calendar;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;

import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;
import org.springmodules.jcr.JcrCallback;
import org.springmodules.jcr.JcrConstants;
import org.springmodules.jcr.JcrTemplate;

import com.genia.toolbox.basics.bean.DataContainer;
import com.genia.toolbox.basics.bean.impl.InputStreamDataContainer;
import com.genia.toolbox.basics.bean.impl.InputStreamFactoryDataContainer;
import com.genia.toolbox.basics.exception.technical.TechnicalException;
import com.genia.toolbox.basics.exception.technical.TechnicalIOException;
import com.genia.toolbox.basics.exception.technical.WrapperException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.StreamManager;
import com.genia.toolbox.persistence.jcr.manager.DataContainerManager;
import com.genia.toolbox.spring.provider.init.bean.impl.AbstractSpringInitializable;

/**
 * implementation of {@link DataContainerManager}.
 */
public class DataContainerManagerImpl
    extends AbstractSpringInitializable
    implements DataContainerManager
{

  /**
   * the <code>Log</code> variabe.
   */
  private static final Logger LOGGER = LoggerFactory.getLogger(DataContainerManagerImpl.class);

  /**
   * the name of the folder created for storing new directory structure.
   */
  private static final String NEW_DIRECTORY_NAME = "directory";

  /**
   * {@link Random} element to use.
   */
  private static final Random RANDOM = new Random();

  /**
   * relative path to the node containing the content.
   */
  private static final String RELATIVE_PATH_TO_CONTENTS = "content";

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;

  /**
   * the {@link JcrTemplate} to use.
   */
  private JcrTemplate jcrTemplate;

  /**
   * the {@link StreamManager} to use.
   */
  private StreamManager streamManager;



  /**
   * delete the information linked to a node identified by its uuid.
   * 
   * @param uuid
   *          the uuid of the persisted {@link DataContainer}
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.web.persistence.jcr.manager.DataContainerManager#delete(java.lang.String)
   */
  public void delete(final String uuid)
      throws TechnicalException
  {
    try {
      jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          Node fileNode;
          try {
            fileNode = session.getNodeByUUID(uuid);
          }
          catch (ItemNotFoundException e) {
            return null;
          }
          fileNode.remove();
          session.save();
          return null;
        }

      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * returns the {@link DataContainer} associated to an uuid, or
   * <code>null</code> if no such object exists.
   * 
   * @param uuid
   *          the uuid of the {@link DataContainer} to retrieve
   * @return the {@link DataContainer} associated to an uuid, or
   *         <code>null</code> if no such object exists
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.web.persistence.jcr.manager.DataContainerManager#getDataContainer(java.lang.String)
   */
  public DataContainer getDataContainer(final String uuid)
      throws TechnicalException
  {
    try {
      return (DataContainer) jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          Node fileNode;
          try {
            fileNode = session.getNodeByUUID(uuid);
          }
          catch (ItemNotFoundException e) {
            return null;
          }
          JcrConstants jcrConstants = new JcrConstants(session);
          return getDataContainer(jcrConstants, fileNode);
        }

      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * returns the {@link DataContainer} representing the file at the given path
   * in the given directory.
   * 
   * @param directoryUuid
   *          the uuid of the directory
   * @param path
   *          the path of the file
   * @return the {@link DataContainer} representing the file at the given path
   *         in the given directory or <code>null</code> if no such directory
   *         of file exists
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.persistence.jcr.manager.DataContainerManager#getDataContainerFromDirectory(java.lang.String,
   *      java.lang.String)
   */
  public DataContainer getDataContainerFromDirectory(final String directoryUuid, final String path)
      throws TechnicalException
  {
    try {
      return (DataContainer) jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          Node directoryNode;
          try {
            directoryNode = session.getNodeByUUID(directoryUuid);
          }
          catch (ItemNotFoundException e) {
            return null;
          }
          Node subNode = getSubNode(directoryNode, path);
          if (subNode == null) {
            return null;
          }
          JcrConstants jcrConstants = new JcrConstants(session);
          return getDataContainer(jcrConstants, subNode);
        }

      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * getter for the jcrTemplate property.
   * 
   * @return the jcrTemplate
   */
  public JcrTemplate getJcrTemplate()
  {
    return jcrTemplate;
  }



  /**
   * returns a random {@link DataContainer} representing a file immediatly under
   * the given path in the given directory. This should be called on a leaf
   * directory if the result must be equitable.
   * 
   * @param directoryUuid
   *          the uuid of the directory
   * @param path
   *          the path of the file
   * @return a random {@link DataContainer} representing a file immediatly under
   *         the given path in the given directory or <code>null</code> if no
   *         data is available
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.persistence.jcr.manager.DataContainerManager#getRandomDataContainerFromDirectory(java.lang.String,
   *      java.lang.String)
   */
  public DataContainer getRandomDataContainerFromDirectory(final String directoryUuid, final String path)
      throws TechnicalException
  {
    try {
      return (DataContainer) jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          Node directoryNode;
          try {
            directoryNode = session.getNodeByUUID(directoryUuid);
          }
          catch (ItemNotFoundException e) {
            return null;
          }
          Node subNode = getSubNode(directoryNode, path);
          if (subNode == null) {
            return null;
          }
          JcrConstants jcrConstants = new JcrConstants(session);
          Node node = getRandomSubNode(subNode, jcrConstants.getNT_FILE());
          if (node == null) {
            return null;
          }
          return getDataContainer(jcrConstants, node);
        }

      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * getter for the streamManager property.
   * 
   * @return the streamManager
   */
  public StreamManager getStreamManager()
  {
    return streamManager;
  }



  /**
   * The method to be called at the start of the application.
   * 
   * @see com.genia.toolbox.spring.provider.init.bean.SpringInitializable#init()
   */
  public void init()
  {
    try {
      jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException, FileNotFoundException
        {
          if (!session.getRootNode().hasNode(RELATIVE_PATH_TO_CONTENTS)) {
            session.getRootNode().addNode(RELATIVE_PATH_TO_CONTENTS);
            session.save();
          }
          return null;
        }
      });
    }
    catch (Exception e) {
      LOGGER.error(e.getMessage(), e);
    }
  }



  /**
   * returns the uuid of a new Node that can be used as a root for a new
   * directory structure.
   * 
   * @return the uuid of a new Node that can be used as a root for a new
   *         directory structure
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.persistence.jcr.manager.DataContainerManager#newDirectory()
   */
  @Transactional(rollbackFor = TechnicalException.class)
  public String newDirectory()
      throws TechnicalException
  {
    try {
      return (String) jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          JcrConstants jcrConstants = new JcrConstants(session);
          Node folderNode = session.getRootNode().getNode(RELATIVE_PATH_TO_CONTENTS);
          Node directoryNode = folderNode.addNode(NEW_DIRECTORY_NAME, jcrConstants.getNT_FOLDER());
          directoryNode.addMixin(jcrConstants.getMIX_REFERENCEABLE());
          session.save();
          return directoryNode.getUUID();
        }
      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * persist a {@link DataContainer}.
   * 
   * @param dataContainer
   *          the {@link DataContainer} to persist
   * @return the uuid of the persisted {@link DataContainer}
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.web.persistence.jcr.manager.DataContainerManager#persistDataContainer(com.genia.toolbox.basics.bean.DataContainer)
   */
  @Transactional(rollbackFor = TechnicalException.class)
  public String persistDataContainer(final DataContainer dataContainer)
      throws TechnicalException
  {
    try {
      return (String) jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          try {
            JcrConstants jcrConstants = new JcrConstants(session);
            Node folderNode = session.getRootNode().getNode(RELATIVE_PATH_TO_CONTENTS);
            Node fileNode = folderNode.addNode(dataContainer.getName(), jcrConstants.getNT_FILE());
            fileNode.addMixin(jcrConstants.getMIX_REFERENCEABLE());
            addNodeContent(jcrConstants, fileNode, dataContainer);
            session.save();
            return fileNode.getUUID();
          }
          catch (TechnicalException e) {
            throw new WrapperException(e);
          }
        }

      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * setter for the jcrTemplate property.
   * 
   * @param jcrTemplate
   *          the jcrTemplate to set
   */
  public void setJcrTemplate(JcrTemplate jcrTemplate)
  {
    this.jcrTemplate = jcrTemplate;
  }



  /**
   * setter for the streamManager property.
   * 
   * @param streamManager
   *          the streamManager to set
   */
  public void setStreamManager(StreamManager streamManager)
  {
    this.streamManager = streamManager;
  }



  /**
   * upload the content of a zip encoded file to the the given directory. If the
   * directory is not null, all its content will be deleted.
   * 
   * @param directoryUuid
   *          the uuid of the directory to upload the content of the file to
   * @param zipDataContainer
   *          the {@link DataContainer} containing the zipped datas
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.persistence.jcr.manager.DataContainerManager#uploadZipDataContainer(java.lang.String,
   *      com.genia.toolbox.basics.bean.DataContainer)
   */
  @Transactional(rollbackFor = TechnicalException.class)
  public void uploadZipDataContainer(final String directoryUuid, final DataContainer zipDataContainer)
      throws TechnicalException
  {
    try {
      jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          try {
            ZipInputStream zipInputStream = new ZipInputStream(zipDataContainer.getInputStream());

            ZipEntry zipEntry = zipInputStream.getNextEntry();

            if (zipEntry == null) {
              // throw an exception as trying to upload an empty file is not
              // useful.
              throw new TechnicalIOException(new ZipException("The given dataContainer was not a zip."));
            }

            JcrConstants jcrConstants = new JcrConstants(session);

            deleteDirectoryContent(directoryUuid);

            Node directoryNode = session.getNodeByUUID(directoryUuid);

            while (zipEntry != null) {
              if (!zipEntry.isDirectory()) {
                Node fileNode = createFileNode(jcrConstants, directoryNode, zipEntry.getName());
                InputStreamDataContainer dataContainer = new InputStreamDataContainer();
                dataContainer.setName(fileNode.getName());
                dataContainer.setInputStream(zipInputStream);
                addNodeContent(jcrConstants, fileNode, dataContainer);
              }
              zipEntry = zipInputStream.getNextEntry();
            }
            session.save();
            zipInputStream.close();
            return null;
          }
          catch (Exception e) {
            throw new WrapperException(e);
          }
        }

      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * fill the content of a file node with the data from a {@link DataContainer}.
   * 
   * @param jcrConstants
   *          the {@link JcrConstants} that allow us to retrieve the name of the
   *          properties
   * @param fileNode
   *          the node to fill
   * @param dataContainer
   *          the {@link DataContainer} containing the value
   * @throws RepositoryException
   *           if an error occured
   * @throws TechnicalException
   *           if an error occured
   */
  private void addNodeContent(JcrConstants jcrConstants, Node fileNode, DataContainer dataContainer)
      throws RepositoryException, TechnicalException
  {
    Node resNode = fileNode.addNode(jcrConstants.getJCR_CONTENT(), jcrConstants.getNT_RESOURCE());
    resNode.setProperty(jcrConstants.getJCR_DATA(), dataContainer.getInputStream());
    resNode.setProperty(jcrConstants.getJCR_MIMETYPE(), dataContainer.getContentType());
    Calendar lastModified = Calendar.getInstance();
    lastModified.setTimeInMillis(System.currentTimeMillis());
    resNode.setProperty(jcrConstants.getJCR_LASTMODIFIED(), lastModified);
  }



  /**
   * create a new file {@link Node} in the given path relative to the directory
   * node given in parameter.
   * 
   * @param jcrConstants
   *          the {@link JcrConstants} that allow us to retrieve the name of the
   *          properties
   * @param directoryNode
   *          the base node
   * @param relativePath
   *          the path from the base node to the file node
   * @return the created node
   * @throws RepositoryException
   *           of an error occured
   */
  private Node createFileNode(final JcrConstants jcrConstants, final Node directoryNode, final String relativePath)
      throws RepositoryException
  {
    final StringTokenizer stringTokenizer = new StringTokenizer(getCorrectPath(relativePath), "/");
    Node currentNode = directoryNode;
    String pathToken = null;
    while (stringTokenizer.hasMoreTokens()) {
      pathToken = stringTokenizer.nextToken();
      if (stringTokenizer.hasMoreTokens()) {
        if (!currentNode.hasNode(pathToken)) {
          currentNode.addNode(pathToken, jcrConstants.getNT_FOLDER());
        }
        currentNode = currentNode.getNode(pathToken);
      }
    }
    return currentNode.addNode(pathToken, jcrConstants.getNT_FILE());
  }



  /**
   * delete the content of the directory referenced by the uuid given in
   * parameter.
   * 
   * @param uuid
   *          the uuid of the directory to delete
   * @throws TechnicalException
   *           when an error occured
   */
  private void deleteDirectoryContent(final String uuid)
      throws TechnicalException
  {
    try {
      jcrTemplate.execute(new JcrCallback()
      {
        public Object doInJcr(Session session)
            throws RepositoryException
        {
          Node fileNode;
          try {
            fileNode = session.getNodeByUUID(uuid);
          }
          catch (ItemNotFoundException e) {
            return null;
          }
          NodeIterator nodeIdIterator = fileNode.getNodes();
          while (nodeIdIterator.hasNext()) {
            nodeIdIterator.nextNode().remove();
          }
          session.save();
          return null;
        }

      });
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }

  }



  /**
   * returns a relative path by stripping leading slashes.
   * 
   * @param path
   *          the path to transform
   * @return the given path without the leading slashes.
   */
  private String getCorrectPath(String path)
  {
    if (path == null) {
      return path;
    }
    return path.replaceFirst("^//*", "");
  }



  /**
   * returns a {@link DataContainer} representing the datas of the {@link Node}
   * given in parameter.
   * 
   * @param jcrConstants
   *          the {@link JcrConstants} that allow us to retrieve the name of the
   *          properties
   * @param fileNode
   *          the node containing the datas
   * @return a {@link DataContainer} representing the datas of the {@link Node}
   *         given in parameter
   * @throws RepositoryException
   *           if a problem occured
   */
  private DataContainer getDataContainer(JcrConstants jcrConstants, Node fileNode)
      throws RepositoryException
  {
    InputStreamFactoryDataContainer dataContainer = new InputStreamFactoryDataContainer();
    dataContainer.setName(fileNode.getName());
    Node contentNode = fileNode.getNode(jcrConstants.getJCR_CONTENT());
    try {
      dataContainer.setContentType(contentNode.getProperty(jcrConstants.getJCR_MIMETYPE()).getString());
      dataContainer.setInputStreamFactory(getStreamManager().cloneInputStream(contentNode.getProperty(jcrConstants.getJCR_DATA()).getStream()));
    }
    catch (TechnicalException e) {
      throw new WrapperException(e);
    }
    return dataContainer;
  }



  /**
   * returns a random sub node of the {@link Node} given in parameter having the
   * fiven type.
   * 
   * @param root
   *          the {@link Node} to search a sub node from
   * @param type
   *          the type of the {@link Node} to return
   * @return a random sub node of the {@link Node} given in parameter having the
   *         fiven type
   * @throws RepositoryException
   *           if an error occured
   */
  private Node getRandomSubNode(final Node root, final String type)
      throws RepositoryException
  {
    long nbSubFile = getSize(root.getNodes());
    if (nbSubFile == 0) {
      return null;
    }
    int element = RANDOM.nextInt((int) nbSubFile);
    NodeIterator nodeIterator = root.getNodes();
    nodeIterator.skip(element);
    while (nodeIterator.hasNext()) {
      Node node = nodeIterator.nextNode();
      if (node.isNodeType(type)) {
        return node;
      }
    }
    // We did not find a node of the right type starting from element, we will
    // try the nodes starting from the begining.
    nodeIterator = root.getNodes();
    while (element > 0) {
      Node node = nodeIterator.nextNode();
      if (node.isNodeType(type)) {
        return node;
      }
      element--;
    }
    return null;
  }



  /**
   * returns the size of the {@link NodeIterator} given in parameter, even when
   * {@link javax.jcr.RangeIterator#getSize()} does not returns any result.
   * 
   * @param nodeIterator
   *          the {@link NodeIterator} to examine
   * @return the size of the {@link NodeIterator} given in parameter
   * @see javax.jcr.RangeIterator#getSize()
   */
  private long getSize(NodeIterator nodeIterator)
  {
    long size = nodeIterator.getSize();
    if (size == -1) {
      while (nodeIterator.hasNext()) {
        nodeIterator.next();
        size++;
      }
    }
    return size;
  }



  /**
   * returns the {@link Node} accessible from rootNode following the given path.
   * This method returns <code>null</code> if the node does not exist.
   * 
   * @param rootNode
   *          the node to start from
   * @param path
   *          the path to the searched node
   * @return the {@link Node} accessible from rootNode following the given path
   * @throws RepositoryException
   *           if an exception occured
   */
  private Node getSubNode(final Node rootNode, final String path)
      throws RepositoryException
  {
    Node res = rootNode;
    String correctPath = getCorrectPath(path);
    if (correctPath != null) {
      if (!rootNode.hasNode(correctPath)) {
        return null;
      }
      res = rootNode.getNode(correctPath);
    }
    return res;
  }

}
